# -*- coding: utf-8 -*-
# SimpleUserCF.py

from AnimeListLoader import AnimeListLoader
from surprise import KNNBasic
import heapq
from collections import defaultdict
from operator import itemgetter

testSubject = '8'
k = 10

# 加载数据集并计算用户相似度矩阵
loader = AnimeListLoader()
data = loader.load_dataset()

trainSet = data.build_full_trainset()

sim_options = {
    'name': 'cosine',
    'user_based': True
}

# 模型是KNN
model = KNNBasic(sim_options=sim_options)
# 训练模型, 拟合数据
model.fit(trainSet)
# 计算相似度矩阵
simsMatrix = model.compute_similarities()

# 获取与测试用户最相似的前 N 个用户
# to_inner_uid 转为 矩阵内部的uid
testUserInnerID = trainSet.to_inner_uid(testSubject)
# 从矩阵中获取最相似的所有用户
similarityRow = simsMatrix[testUserInnerID]  # 用户85的相似度矩阵

similarUsers = []  # 记录相似的用户, 除了当前用户
for innerID, score in enumerate(similarityRow):
    # 排除当前用户 testUserInnerID
    if (innerID != testUserInnerID):
        similarUsers.append((innerID, score))

# 找到前k个最相似的user
kNeighbors = heapq.nlargest(k, similarUsers, key=lambda t: t[1])

# 获取这些用户评分的项目，并根据用户相似度对每个项目的评分进行加权
candidates = defaultdict(float)  # 候选评分列表
# 遍历相似用户
for similarUser in kNeighbors:
    innerID = similarUser[0]  # 拿到id
    userSimilarityScore = similarUser[1]  # 和85号的相似分值(有多相似)
    theirRatings = trainSet.ur[innerID]  # 该相似用户对物品的评分数据
    for rating in theirRatings:  # rating[0] -> innerID, rating[1] -> rating
        # 项目评分：相似用户评分 * 相似度分数
        # (rating / 5.0) 缩放到0~5的范围
        candidates[rating[0]] += (rating[1] / 5.0) * userSimilarityScore

# 构建一个字典，记录该用户已经看过的项目
watched = {}
for itemID, rating in trainSet.ur[testUserInnerID]:  # ur(userRating) 当前用户评分过物品的数据
    watched[itemID] = 1  # 记录85号已经看过的 itemID, 没看过的为 itemID: 0

# 获取来自相似用户的最高评分项目：
print(f"获取top{k}个推荐项目: ")
pos = 0  # 计数
# 根据 candidates候选字典的 [1](score) 排序, 然后遍历
for itemID, ratingSum in sorted(candidates.items(), key=itemgetter(1), reverse=True):
    # 85号没看过
    if not itemID in watched:
        # 转矩阵内部id为原始物品id
        rawID = trainSet.to_raw_iid(itemID)
        print(loader.get_name(int(rawID)), ratingSum)
        pos += 1
        # 得到top10个就够了
        if (pos > 10):
            break
